from utils.idandmeta import IdAndMeta

from stacking.warehouse.crane import Crane
from stacking.warehouse.places import Place, Entrance, Exit, Stack


class Warehouse(IdAndMeta):
    """A warehouse is represented by a set of places (entrances, exits, and stacks) and a set 
    of cranes which carry the items from release to delivery."""
    def __init__(self, id, *objs, **meta):
        IdAndMeta.__init__(self, id, **meta)
        self.objects   = dict()  # {id: [Place|Crane]}
        self.cranes    = set()   # set([Crane])
        self.places    = set()   # set([Place])
        self.entrances = set()   # set([Entrance])
        self.exits     = set()   # set([Exit])
        self.stacks    = set()   # set([Stack])
        self.location  = dict()  # {Item: id}
        self.items     = None    # {id: Item} (this is provided by the Instance object)
        for obj in objs:
            self.add(obj)
            
    def __getitem__(self, key):
        return self.objects[key]
        
    @classmethod
    def quick_build(cls, id, nstacks, ncranes=1, **meta):
        """Creates a simple warehouse with the given number of stacks and cranes."""
        w = cls(id, **meta)
        w.add_place(Entrance("R"))
        w.add_place(Exit("D"))
        for x in xrange(nstacks):
            w.add_place(Stack("S#%d" % x))
        for x in xrange(ncranes):
            w.add_crane(Crane("C#%d" % x))
        return w
        
    def __info__(self):
        lines = [str(self.id)] 
        lines.append("---------- Cranes:")
        lines.extend(str(crane) for crane in sorted(self.cranes, key=lambda c: c.id))
        lines.append("---------- Stacks:")
        lines.extend(str(stack) for stack in sorted(self.stacks, key=lambda s: s.id))
        lines.append("")
        return "\n".join(lines)
        
    def add(self, obj):
        """Add a crane or a place to the warehouse."""
        self._add(obj)
        obj._set_parent(self)
        
    def _add(self, obj):
        if isinstance(obj, Crane):
            self._add_crane(obj)
        elif isinstance(obj, Place):
            self._add_place(obj)
        else:
            raise TypeError("expected Crane or Place")
            
    def add_crane(self, crane):
        self._add_crane(crane)
        crane._set_parent(self)
        
    def _add_crane(self, crane):
        if crane in self.cranes:
            raise KeyError("duplicate crane - %s" % crane)
        self._add_object(crane)
        self.cranes.add(crane)
        
    def add_place(self, place):
        self._add_place(place)
        place._set_parent(self)
        
    def _add_place(self, place):
        if place in self.places:
            raise KeyError("duplicate place - %s" % place)
        self._add_object(place)
        self.places.add(place)
        if place.is_entrance:
            self.entrances.add(place)
        if place.is_exit:
            self.exits.add(place)
        if place.is_stack:
            self.stacks.add(place)
            
    def _add_object(self, obj):
        if obj.id in self.objects:
            raise Exception("object id already in use - %s" % obj.id)
        self.objects[obj.id] = obj
        
    def get_location(self, item):
        """Get an item's current location if it is inside the warehouse, or None otherwise."""
        try:
            location_id = self.location[item]
        except KeyError:
            return None
        return self.objects[location_id]
        
    def set_location(self, item, loc):
        """Update an item's location in the warehouse. If 'loc' is None, the item is considered to 
        be outside the warehouse."""
        if loc is not None:
            self.location[item] = loc.id
        elif item in self.location:
            del self.location[item]
            
    def execute(self, action):
        """Execute a given action (pick up or put down items) in the warehouse. 
        Note that an arbitrary crane is used. While this is fine for the pure SP, it may lead to 
        unexpected results if multiple cranes are used with interleaved actions."""
        crane = iter(self.cranes).next()
        return crane.execute(action)
        
    def deepcopy(self):
        """Create a deep copy of the warehouse in its current state."""
        clone          = Warehouse(self.id, **self.meta)
        clone.location = dict(self.location)
        clone.items    = self.items
        # copy all cranes
        for crane in self.cranes:
            clone.add_crane(crane.deepcopy())
        # copy all places
        for place in self.places:
            clone.add_place(place.deepcopy())
        return clone
        
